import { isArray, noop } from 'lodash-es'
import { ILogger } from '../logger'
import { CompluginMinifyContext, CreateCompluginArgs } from '../types'
import { acornParse, loadDependency, normalizeRawOptions } from '../utils'
import { initCompluginWebpackInjected, LoadProxyFn } from './injected'
import {
  doResolve,
  emitAsset,
  emitChunk,
  emitStatic,
  ensureCompilerWebpack,
  PLUGIN_KEY,
  TRANSFORM_LOADER
} from './utils'

export default function getWebpackPlugin<UserOptions = {}>(
  createCompluginArgs: CreateCompluginArgs<UserOptions>,
  options?: UserOptions
) {
  const { factory } = createCompluginArgs
  let tmpName: string
  let tmpEnforce: 'pre' | 'post' | undefined

  const webpackPlugin: import('webpack').WebpackPluginInstance & { enforce?: 'pre' | 'post' } = {
    get name() {
      return tmpName ?? (tmpName = createCompluginArgs.name)
    },
    set name(value) {
      tmpName = value
    },
    get enforce() {
      return tmpEnforce ?? (tmpEnforce = createCompluginArgs.enforce)
    },
    set enforce(value) {
      tmpEnforce = value
    },
    apply(compiler) {
      ensureCompilerWebpack(compiler)

      const { name } = createCompluginArgs
      const pluginName = 'complugin:' + name
      const pluginLogger = compiler.getInfrastructureLogger(`${name}:webpack`)
      const hookLoggerMap: Record<string, ILogger> = Object.create(null)
      const getHookLogger = (hook: string) =>
        hookLoggerMap[hook] ?? (hookLoggerMap[hook] = compiler.getInfrastructureLogger(`${name}:webpack/${hook}`))
      const injected = initCompluginWebpackInjected(compiler)
      const meta = Object.create(null, {
        ...Object.getOwnPropertyDescriptors(injected.meta),
        logger: { configurable: true, enumerable: true, value: pluginLogger }
      })
      const rawPlugin = normalizeRawOptions(factory.call(meta, options, meta))
      const { buildStart, buildEnd, transformInclude, transform, load, resolveId, generateBundle } = rawPlugin
      injected.plugins[name] = {
        compluginName: name,
        pluginName,
        transform,
        transformLogger: getHookLogger('transform')
      }

      const getMinifyContext = (compilation: import('webpack').Compilation, hook: string): CompluginMinifyContext => {
        const logger = getHookLogger(hook)
        return {
          logger,
          parse: acornParse(),
          resolve: (source, importer) => doResolve(source, importer, compilation),
          emitFile: asset => void emitAsset(asset, compilation),
          error: message => {
            logger.error(message)
          },
          warn: message => {
            logger.warn(message)
          },
          // @ts-ignore
          addWatchFile: noop,
          getWatchFiles: () => []
        }
      }

      if (buildStart) {
        injected.onBuildStarts(
          compilation => buildStart.call(getMinifyContext(compilation, 'buildStart')),
          this.enforce
        )
      }

      if (buildEnd) {
        injected.onBuildEnds(compilation => buildEnd.call(getMinifyContext(compilation, 'buildEnd')), this.enforce)
      }

      if (generateBundle) {
        const boundGenerateBundle = generateBundle.bind({
          logger: hookLoggerMap.transformInclude ?? (hookLoggerMap.transformInclude = getHookLogger('generateBundle'))
        })
        injected.onGenerateBundle(boundGenerateBundle, this.enforce)
      }

      if (resolveId) {
        let loadProxy: LoadProxyFn | undefined
        if (load) {
          loadProxy = function (id, resource, compilation) {
            return load.call(
              {
                ...getMinifyContext(compilation, 'load'),
                sourceMap: true,
                emitAsset: asset => emitAsset(asset, compilation),
                emitChunk: id => emitChunk(id, compilation),
                emitIife: id => emitChunk(id, compilation, { chunkLoading: false, library: { type: 'var' } }),
                emitStatic: fileName => emitStatic(fileName, compilation),
                addWatchFile: fileName => injected.addWatchFile(resource, fileName),
                rebuild: () => {
                  const resource = injected.getRequestVirtualResource(id)
                  if (resource == null) return

                  injected.rebuild(resource, compilation)
                }
              },
              id
            )
          }
        }
        injected.onResolveIdAndLoads(
          (compilation, ...args) => resolveId.apply(getMinifyContext(compilation, 'resolveId'), args),
          loadProxy,
          this.enforce
        )
      }

      if (transform) {
        const boundTransformInclude = transformInclude.bind({
          logger: getHookLogger('transformInclude')
        })
        injected.addRule({
          include: (resource: string) => {
            if (!resource) return false

            const id = injected.virtualResourceToRequest(resource) ?? resource
            return Boolean(boundTransformInclude(id))
          },
          enforce: this.enforce,
          type: 'javascript/auto',
          use: { loader: TRANSFORM_LOADER, options: { [PLUGIN_KEY]: name } }
        })
      }
    }
  }

  return webpackPlugin
}

const webpackDevServerPrototype = loadDependency('webpack-dev-server')?.prototype
if (webpackDevServerPrototype) {
  if (webpackDevServerPrototype) {
    let compiler: import('webpack').Compiler

    const setDevServer = (compiler: any, devServer: any) => {
      if (compiler) {
        compiler.$devServer = devServer
        const compilers = compiler.compilers
        if (isArray(compilers)) {
          compilers.forEach(compiler => setDevServer(compiler, devServer))
        }
      }
    }

    Object.defineProperty(webpackDevServerPrototype, 'compiler', {
      configurable: true,
      get() {
        return compiler
      },
      set(this: any, value) {
        compiler = value
        setDevServer(compiler, this)
      }
    })

    const oldListen = webpackDevServerPrototype.listen
    if (oldListen) {
      const listen = (webpackDevServerPrototype.listen = function listen(...args: any[]) {
        setDevServer(this.compiler, this)
      })

      Object.defineProperty(listen, 'length', {
        configurable: true,
        value: oldListen.length
      })
    }
  }
}
